﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using System.Diagnostics;
using System.IO;
using System.ServiceModel.Helpers;

namespace Devtm.ServiceModel.Proxy
{
    
    internal class ModuleCreator
    {

        private string filename;
        private string dir;
        private ProxyConfiguration Configuration;

        AssemblyBuilder assembly;
        System.Reflection.Emit.ModuleBuilder module;
        AssemblyName name;
        

        public ModuleCreator(string assemblyName, string filename, Version version, string dir, ProxyConfiguration configuration)
        {

            // TODO: Complete member initialization
            this.filename = filename;
            this.dir = dir;
            this.Configuration = configuration;

            name = new AssemblyName(assemblyName);
            name.Version = version;
            assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);
            module = assembly.DefineDynamicModule("module");
            
        }


        public void Save()
        {
            assembly.Save(filename);        
        }


        internal Type Get(Type typeBase, Type typeInterface)
        {

            HashSet<string> list = new HashSet<string>();

            Type result = null;

            string typeName = "DynamicWrapper" + typeInterface.Name;

            TypeBuilder tb = module.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Public, typeBase);

            try
            {

                GenerateCtors(typeInterface, typeBase, tb, typeBase.GetConstructors(BindingFlags.Public | BindingFlags.Instance));
                GenerateCtors(typeInterface, typeBase, tb, typeBase.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance));
                GenerateMethods(typeInterface, typeInterface, typeBase, tb, typeInterface.GetMethods(), list);

                tb.AddInterfaceImplementation(typeInterface);
                
                result = tb.CreateType();
            }
            catch (Exception e)
            {

                StackTrace s = new StackTrace(e);

            }
            finally
            {
                
            }

            return result;
        }


        #region Parameters

        private static Type[] GetTypesParameters(ParameterInfo[] parameters)
        {

            List<Type> parametersList = new List<Type>();
            
            foreach (ParameterInfo p in parameters)
                parametersList.Add(p.ParameterType);

            Type[] ps = parametersList.ToArray();

            return ps;

        }


        private void SetParameters(ParameterInfo[] parameters, ConstructorBuilder meth)
        {
            for (int i = 0; i < parameters.Length; i++)
            {
                var p = parameters[i];
                meth.DefineParameter(i, p.Attributes, p.Name);
            }
        }


        private void SetParameters(ParameterInfo[] parameters, MethodBuilder meth)
        {
            for (int i = 0; i < parameters.Length; i++)
            {
                var p = parameters[i];
                meth.DefineParameter(i, p.Attributes, p.Name);
            }
        }

        #endregion


        private void GenerateCtors(Type typeInterface, Type typeBase, TypeBuilder tb, ConstructorInfo[] constructorInfo)
        {

            foreach (ConstructorInfo m in constructorInfo)
                GenerateCtor(typeInterface, typeBase, tb, m);

        }


        private void GenerateCtor(Type typeInterface, Type typeBase, TypeBuilder tb, ConstructorInfo m)
        {


            ParameterInfo[] parameters = m.GetParameters();

            var ctor = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, GetTypesParameters(parameters));
            SetParameters(parameters, ctor);

            ILGenerator gen = ctor.GetILGenerator();

            gen.Emit(OpCodes.Nop);
            gen.Emit(OpCodes.Ldarg_0);
            SendArguments(gen, parameters);
            gen.Emit(OpCodes.Call, m);
            gen.Emit(OpCodes.Ret);


        }


        private void GenerateMethods(Type Interface, Type typeInterface, Type typeBase, TypeBuilder tb, MethodInfo[] methodInfos, HashSet<string> list)
        {

            foreach (MethodInfo m in methodInfos)            
                if (!list.Contains(m.ToString()))
                    GenerateMethod(Interface, typeInterface, typeBase, tb, m, list);
            
            var ii = Interface.GetInterfaces();

            foreach (Type i in ii)            
                GenerateMethods(i, typeInterface, typeBase, tb, i.GetMethods(), list);
            
        }


        private void GenerateMethod(Type Interface, Type typeInterface, Type typeBase, TypeBuilder tb, MethodInfo m, HashSet<string> list)
        {


            var _events = Configuration.GetEventHandlerOperation(m);

            list.Add(m.ToString());

            MethodInfo m1 = typeBase.GetMethod("CreateInvoker");


            ParameterInfo[] parameters = m.GetParameters();
            var meth = tb.DefineMethod(m.Name, MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.Standard, m.ReturnType, GetTypesParameters(parameters));            
            SetParameters(parameters, meth);

            ILGenerator gen = meth.GetILGenerator();
            LocalBuilder varRetour = null;
            LocalBuilder varTestIntercept = gen.DeclareLocal(typeof(bool));
            LocalBuilder varService = gen.DeclareLocal(typeInterface);
            LocalBuilder varArgs = gen.DeclareLocal(typeof(object[]));
            LocalBuilder varCatchBefore = gen.DeclareLocal(typeof(Devtm.ServiceModel.Proxy.Events.InterceptBeforeEventArgs));
            LocalBuilder varException = gen.DeclareLocal(typeof(Exception));
            Label returnLabel = gen.DefineLabel();
            Label continueLabel = gen.DefineLabel();
            Label BeginFinallyBlockLabel = gen.DefineLabel();

            if (m.ReturnType.Name != "Void")            
                varRetour = gen.DeclareLocal(m.ReturnType);


            // --- Start ---
            gen.Emit(OpCodes.Nop);


            // set new array
            gen.WriteInteger(m.GetParameters().Count());
            gen.Emit(OpCodes.Newarr, typeof(object));
            gen.SetVariable(varArgs);
            // Fill array
            SendArgumentsToArray(gen, varArgs, parameters);


            #region Log

            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldstr, "Begin");
            gen.Emit(OpCodes.Ldstr, m.DeclaringType.FullName);
            gen.Emit(OpCodes.Ldstr, m.ToString());
            gen.GetVariable(varArgs);
            gen.Emit(OpCodes.Call, typeof(ClientBase).GetMethod("Log"));

            #endregion


            #region Intercept before

            if (_events.HasInterceptBeforeSubscription)
            {
                gen.Emit(OpCodes.Ldarg_0);
                gen.GetVariable(varService);                
                gen.GetVariable(varArgs);
                gen.Emit(OpCodes.Call, typeBase.GetMethod("CatchBefore"));
                gen.SetVariable(varCatchBefore);

                // on load la propriete InterceptResult
                gen.GetVariable(varCatchBefore);
                gen.Emit(OpCodes.Call, typeof(Devtm.ServiceModel.Proxy.Events.InterceptBeforeEventArgs)
                    .GetMethod("get_InterceptResult"));
                gen.Emit(OpCodes.Ldc_I4_0);
                // compare le if
                gen.Emit(OpCodes.Ceq);
                gen.SetVariable(varTestIntercept);

                // !If
                gen.GetVariable(varTestIntercept);
                gen.Emit(OpCodes.Brtrue_S, continueLabel);
                if (varRetour != null)
                {
                    gen.Emit(OpCodes.Nop);
                    gen.GetVariable(varCatchBefore);
                    gen.Emit(OpCodes.Call, typeof(Devtm.ServiceModel.Proxy.Events.InterceptBeforeEventArgs)
                        .GetMethod("GetResult")
                        .MakeGenericMethod(m.ReturnType));
                    gen.SetVariable(varRetour);
                }

                gen.Emit(OpCodes.Br_S, returnLabel);    // on saute a la fin
                gen.Emit(OpCodes.Nop);
                gen.MarkLabel(continueLabel);

            }

            #endregion


            // --- Create channel ---
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Call, m1);
            gen.SetVariable(varService);


            // --- Begin try ---
            Label beginTry = gen.BeginExceptionBlock();


            // --- Call ---
            gen.GetVariable(varService);
            SendArguments(gen, parameters);
            gen.Emit(OpCodes.Callvirt, m);
            if (varRetour != null)
                gen.SetVariable(varRetour);


            #region Intercept Catch

            if (_events.HasExceptionSubscription)
            {

                // On stock l'exception
                gen.BeginCatchBlock(typeof(Exception));
                gen.SetVariable(varException);

                gen.Emit(OpCodes.Ldarg_0);
                gen.GetVariable(varException);
                gen.GetVariable(varService);
                gen.GetVariable(varArgs);

                gen.Emit(OpCodes.Call, typeof(ClientBase).GetMethod("CatchException"));

                // on leve l'exception
                gen.GetVariable(varException);
                gen.Emit(OpCodes.Throw);

            }

            #endregion


            // --- Begin finally ---
            gen.BeginFinallyBlock();
            gen.MarkLabel(BeginFinallyBlockLabel);


            // --- Close connection ---
            gen.Emit(OpCodes.Ldarg_0);
            gen.GetVariable(varService);
            gen.Emit(OpCodes.Isinst, typeof(System.ServiceModel.ICommunicationObject));
            gen.Emit(OpCodes.Call, typeof(ClientBase).GetMethod("FinalizeInvoker"));


            #region Intercept After

            if (_events.HasInterceptAfterSubscription)
            {

                gen.Emit(OpCodes.Ldarg_0);
                gen.GetVariable(varService);

                if (varRetour != null)
                    gen.GetVariable(varRetour);
                
                gen.GetVariable(varArgs);

                if (varRetour != null)

                    gen.Emit(OpCodes.Call, typeof(ClientBase).GetMethod("CatchAfter").MakeGenericMethod(m.ReturnType));
                else
                    gen.Emit(OpCodes.Call, typeof(ClientBase).GetMethod("CatchAfterVoid"));                
               
            }

            #endregion


            #region Log

            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldstr, "Exit");
            gen.Emit(OpCodes.Ldstr, m.DeclaringType.FullName);
            gen.Emit(OpCodes.Ldstr, m.ToString());
            gen.GetVariable(varArgs);
            gen.Emit(OpCodes.Call, typeof(ClientBase).GetMethod("Log"));

            #endregion


            gen.EndExceptionBlock();


            // --- out ---
            gen.MarkLabel(returnLabel);
            if (varRetour != null)
                gen.GetVariable(varRetour);
            
            gen.Emit(OpCodes.Ret);

        }


        private void SendArguments(ILGenerator gen, ParameterInfo[] parameters)
        {

            foreach (ParameterInfo item in parameters)            
                gen.WriteParameterArgument(item);
            
        }

        private void SendArgumentsToArray(ILGenerator gen, LocalBuilder varArgs, ParameterInfo[] parameters)
        {


            foreach (ParameterInfo item in parameters)
            {
                gen.GetVariable(varArgs);
                gen.WriteInteger(item.Position);
                gen.WriteParameterArgument(item);


                if (item.ParameterType == typeof(int) || item.ParameterType.IsEnum)
                {
                    gen.Emit(OpCodes.Box, item.ParameterType);
                }
                                   
                gen.Emit(OpCodes.Stelem_Ref);
                


            }

        }

    }
}
